﻿/*************************************************************************************
 * 工    具：  HK_TMPGradient
 * 
 * 描    述：  HK_TMPGradient 是一个用于为 TextMeshPro 文本添加渐变效果的工具脚本。
 *            根据当前Unity工程是否导入TextMeshPro，自动进行预编译判定，
 *            允许用户自定义渐变的起始颜色、结束颜色、渐变角度以及渐变范围，
 *            解决TMP_Text默认支持逐个字符渐变的问题，
 *            支持在编辑模式和运行时实时更新效果。当文本内容改变或属性值调整时，
 *            脚本会自动重新计算并应用渐变效果。
 * 
 * 版    本：  V1.0
 * 作    者：  京产肠饭
 * 
 * 创    建：  2025/07/09  V1.0
 * 
 * 链    接：  https://gitee.com/JingChanChangFan/hk_-unity-tools
*************************************************************************************/

using UnityEngine;

#if TMPIMPORT
using TMPro;
#endif

namespace HKTools
{
#if TMPIMPORT
    [ExecuteAlways]
    [RequireComponent(typeof(TMP_Text))]
    public class HK_TMPGradient : MonoBehaviour
    {
        [SerializeField] Color startColor = Color.white;
        [SerializeField] Color endColor = Color.black;

        [SerializeField, Range(0, 360)] float angle = 0f;

        [SerializeField, Range(0f, 1f)] float rangeStart = 0.0f;
        [SerializeField, Range(0f, 1f)] float rangeEnd = 1.0f;

        TMP_Text tmp;
        bool isDirty = true;
        Color prevStartColor;
        Color prevEndColor;
        float prevAngle;
        float prevRangeStart;
        float prevRangeEnd;

        void OnEnable()
        {
            tmp = GetComponent<TMP_Text>();
            TMPro_EventManager.TEXT_CHANGED_EVENT.Add(OnTextChanged);
            CachePreviousValues();
        }

        void OnDisable()
        {
            TMPro_EventManager.TEXT_CHANGED_EVENT.Remove(OnTextChanged);
        }

        void OnValidate()
        {
            isDirty = true;
        }

        void Update()
        {
            if (!isDirty && !HasValuesChanged())
                return;

            Apply();
            CachePreviousValues();
        }

        void OnTextChanged(Object obj)
        {
            if (obj == tmp) isDirty = true;
        }

        void Apply()
        {
            if (tmp == null)
                return;

            tmp.ForceMeshUpdate();
            var info = tmp.textInfo;

            // Step1: 求文本包围盒  
            var bounds = GetTextBounds(info);

            // Step2: 方向向量 & 极值投影  
            var (direction, minDot, maxDot, dotLength) = CalculateProjection(bounds);

            // Step3: 遍历顶点写色  
            UpdateVertexColors(info, direction, minDot, maxDot, dotLength);

            isDirty = false;
        }

        Bounds GetTextBounds(TMP_TextInfo info)
        {
            Bounds bounds = new Bounds(tmp.transform.position, Vector3.zero);
            for (int i = 0; i < info.characterCount; i++)
            {
                if (!info.characterInfo[i].isVisible) continue;
                var verts = info.meshInfo[info.characterInfo[i].materialReferenceIndex].vertices;
                for (int j = 0; j < 4; j++)
                {
                    Vector3 v = verts[info.characterInfo[i].vertexIndex + j];
                    bounds.Encapsulate(v);
                }
            }
            return bounds;
        }

        (Vector2 direction, float minDot, float maxDot, float dotLength) CalculateProjection(Bounds bounds)
        {
            float rad = angle * Mathf.Deg2Rad;
            Vector2 direction = new(Mathf.Cos(rad), Mathf.Sin(rad));

            Vector2[] corners = {
                new(bounds.min.x, bounds.min.y), new(bounds.min.x, bounds.max.y),
                new(bounds.max.x, bounds.min.y), new(bounds.max.x, bounds.max.y)
            };

            float minDot = float.MaxValue, maxDot = float.MinValue;

            foreach (var corner in corners)
            {
                float dotProduct = Vector2.Dot(corner, direction);
                minDot = Mathf.Min(minDot, dotProduct);
                maxDot = Mathf.Max(maxDot, dotProduct);
            }

            float dotLength = maxDot - minDot;
            if (dotLength < Mathf.Epsilon)
            {
                dotLength = Mathf.Epsilon;
            }

            return (direction, minDot, maxDot, dotLength);
        }

        void UpdateVertexColors(TMP_TextInfo info, Vector2 direction, float minDot, float maxDot, float dotLength)
        {
            float a = Mathf.Min(rangeStart, rangeEnd);
            float b = Mathf.Max(rangeStart, rangeEnd);

            for (int m = 0; m < info.meshInfo.Length; m++)
            {
                var meshInfo = info.meshInfo[m];
                var verts = meshInfo.vertices;
                var cols32 = meshInfo.colors32;

                for (int i = 0; i < meshInfo.vertexCount; i++)
                {
                    float tRaw = (Vector2.Dot(verts[i], direction) - minDot) / dotLength;
                    float t = Mathf.InverseLerp(a, b, tRaw);
                    cols32[i] = Color32.Lerp(startColor, endColor, t);
                }
                meshInfo.mesh.colors32 = cols32;
                tmp.UpdateGeometry(meshInfo.mesh, m);
            }
        }

        bool HasValuesChanged()
        {
            return startColor != prevStartColor ||
                   endColor != prevEndColor ||
                   angle != prevAngle ||
                   rangeStart != prevRangeStart ||
                   rangeEnd != prevRangeEnd;
        }

        void CachePreviousValues()
        {
            prevStartColor = startColor;
            prevEndColor = endColor;
            prevAngle = angle;
            prevRangeStart = rangeStart;
            prevRangeEnd = rangeEnd;
        }
    }
#endif
}